import asyncio
from functools import partial
import hashlib
import json
import logging
import logging.config
import random
import string
import uuid
from urllib.parse import urljoin

from tornado import gen
import tornado.options
from tornado.websocket import websocket_connect
from tornado.options import define, options
from tornado.ioloop import IOLoop

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simpleFormatter': {
            'format': '[%(asctime)s] [%(name)s] [%(levelname)s] - %(message)s',
        },
        'complexFormatter': {
            'format':
            '[%(asctime)s] [%(name)s] [%(levelname)s] [%(module)s:%(lineno)d] - %(message)s',
        },
    },
    'handlers': {
        'fileHandler': {
            'class': 'logging.FileHandler',
            'level': 'DEBUG',
            'formatter': 'complexFormatter',
            'filename': 'smoke_testing.log',
        },
        'consoleHandler': {
            'class': 'logging.StreamHandler',
            'level': 'INFO',
            'formatter': 'simpleFormatter',
        }
    },
    'loggers': {
        'app': {
            'level': 'INFO',
            'handlers': ['consoleHandler', 'fileHandler'],
            'qualname': 'server',
            'propagate': '0',
        },
    },
}
logging.config.dictConfig(LOGGING)

logger = logging.getLogger('app')


def str_equal(value_1, value_2):
    return str(value_1) == str(value_2)


def random_string(size=6, choices=string.ascii_letters):
    return ''.join(random.choice(choices) for _ in range(size))


class WebSocketClient:
    def __init__(self, base_url, room_id, uid, name, quit_event):
        self.id = uid
        self.name = name
        self.conn = None
        self.quit_event = quit_event
        token = hashlib.new('md5', str(uid).encode()).hexdigest()
        url = urljoin(base_url, f'ws/standard/{room_id}')
        self.url = f'{url}?token={token}'
        self.status = {}

    def log(self, level, msg, *args, **kwargs):
        msg = f'[ws: {self.id}] {msg}'
        log_func = getattr(logger, level.lower())
        log_func(msg, *args, **kwargs)

    async def write_json_message(self, message):
        message = json.dumps(message)
        self.log('INFO', 'write_message: %s', message)
        await self.conn.write_message(message)

    async def connect(self):
        self.conn = await websocket_connect(self.url)
        IOLoop.current().add_callback(self.read_callback)
        await self.register()
        await self.post_connect()
        self.conn.close()
        self.log('INFO', 'now quit')

    async def post_connect(self):
        await self.quit_event.wait()

    async def register(self):
        msg = {
            'act': '1',
            'uid': self.id,
            'nm': self.name,
        }
        await self.write_json_message(msg)

    async def read_callback(self):
        while True:
            msg = await self.conn.read_message()
            if msg is None:
                break
            if isinstance(msg, str):
                await self.handle_text_message(msg)
            else:
                await self.handle_binary_message(msg)

    async def handle_text_message(self, msg):
        self.log('INFO', f'[text_message] {msg}')
        try:
            msg_dct = json.loads(msg)
        except json.JSONDecodeError:
            self.log('WARNING', 'non json msg {msg}')
            return
        act = msg_dct.get('act')
        if str_equal(act, 15):
            # 文字聊天
            self.status.setdefault('chat_count', 0)
            self.status['chat_count'] += 1
            assert isinstance(msg_dct.get('total'), int)
            assert isinstance(msg_dct.get('chat_id'), int)
        elif str_equal(act, 18):
            # 图片聊天
            self.status.setdefault('image_chat_count', 0)
            self.status['image_chat_count'] += 1
            assert isinstance(msg_dct.get('total'), int)
            assert isinstance(msg_dct.get('chat_id'), int)
        elif str_equal(act, 7):
            # 状态消息
            self.status['module'] = msg_dct['mod']
            self.status['module_name'] = msg_dct['msg']['mn']
            self.status['module_note'] = msg_dct['msg']['note']
        elif str_equal(act, 40):
            self.status['counter_key'] = msg_dct['data']['key']
            self.status['counter_value'] = msg_dct['data']['total']

    async def handle_binary_message(self, msg):
        self.log('INFO', f'[binary_message] {msg}')

    async def write_chat_message(self, content):
        msg = {
            'act': 15,
            'from': f'{self.id}:{self.name}',
            'msg': {
                'c': content,
                'etag': random_string(size=6),
            },
        }
        await self.write_json_message(msg)

    async def write_image_chat_message(self, url):
        msg = {
            'act': 18,
            'from': f'{self.id}:{self.name}',
            'msg': {
                'c': url,
                'etag': random_string(size=6),
            },
        }
        await self.write_json_message(msg)

    async def write_state_message(self, module_name, **kwargs):
        msg = {
            'act': 7,
            'from': f'{self.id}:{self.name}',
            'to': 'A',
            'mod': module_name,
            'msg': kwargs,
        }
        await self.write_json_message(msg)

    async def write_state_note_message(self, note_id):
        kwargs = {
            'mn': 'NOTE',
            'note': note_id,
        }
        await self.write_state_message('global', **kwargs)

    async def write_counter_message(self, counter_key):
        msg = {
            'act': 40,
            'from': f'{self.id}:{self.name}',
            'data': {
                'key': counter_key
            },
        }
        await self.write_json_message(msg)


class Watcher(WebSocketClient):
    pass


class Sender(WebSocketClient):
    async def post_connect(self,
                           text_chat_count=5,
                           image_chat_count=2,
                           state_note_count=2,
                           counter_count=2):
        for index in range(text_chat_count):
            await self.write_chat_message(f'greeting {index} from {self.name}')
        for index in range(image_chat_count):
            await self.write_image_chat_message(
                f'image {index} from {self.name}')
        for index in range(state_note_count):
            await self.write_state_note_message(index)
        for index in range(counter_count):
            await self.write_counter_message(f'note_{index}')
        await self.conn.write_message('ping')


async def functional_test(base_url, room_id):
    quit_event = asyncio.Event()
    user_id = 1
    watchers = []
    for _ in range(1):
        watcher = Watcher(base_url, room_id, user_id, f'user_{user_id}',
                          quit_event)
        watchers.append(watcher)
        user_id += 1
    for ws in watchers:
        IOLoop.current().add_callback(ws.connect)

    senders = []
    for _ in range(1):
        sender = Sender(base_url, room_id, user_id, f'user_{user_id}',
                        quit_event)
        user_id += 1
        senders.append(sender)
    await gen.multi(ws.connect() for ws in senders)
    logger.info('quit now')
    await asyncio.sleep(10)
    quit_event.set()
    IOLoop.current().stop()


async def stress_test(domain, room_id):
    watcher = Watcher()


def main():
    define('env', 'DEV')
    define('room_id', uuid.uuid4().hex)
    tornado.options.parse_command_line()
    if options.env == 'SIT':
        base_url = 'wss://cc.test.xroom.net:8888'
    elif options.env == 'PRD':
        base_url = 'wss://cc.xroom.net'
    else:
        base_url = 'ws://localhost:8888'
    functional_test_callback = partial(functional_test, base_url,
                                       options.room_id)
    IOLoop.current().add_callback(functional_test_callback)


if __name__ == '__main__':
    main()
    IOLoop.current().start()
